// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';
import 'dart:io';

import 'package:meta/meta.dart';

import '../config/config.dart';
import '../elements/elements.dart';
import '../logging/logging.dart';
import '../util/string_util.dart';
import 'resolver.dart';
import 'visitor.dart';

/// Version of jnigen. Keep in sync with `pubspec.yaml` removing the `-wip`
/// suffix.
@visibleForTesting
const String version = '0.14.2';

// Import prefixes.
const _jni = r'jni$_';
const _core = r'core$_';

// dart:core types
const _override = '@$_core.override';

// package:jni types.
const _jType = '$_jni.JObjType';
const _jPointer = '$_jni.JObjectPtr';
const _jReference = '$_jni.JReference';
const _jGlobalReference = '$_jni.JGlobalReference';
const _jArray = '$_jni.JArray';
const _jObject = '$_jni.JObject';
const _jResult = '$_jni.JniResult';
const _jThrowable = '$_jni.JThrowablePtr';
const _methodInvocation = '$_jni.MethodInvocation';
const _protectedExtension = '$_jni.ProtectedJniExtensions';
const _voidPointer = '$_jni.Pointer<$_jni.Void>';
const _internal = '@$_jni.internal';

// Prefixes and suffixes.
const _typeParamPrefix = '\$';

/// Used for Dart-only bindings.
const _self = 'this';

// Docs.
const _releaseInstruction =
    '  /// The returned object must be released after use, '
    'by calling the [release] method.';

extension on Iterable<String> {
  /// Similar to [join] but adds the [separator] to the end as well.
  String delimited([String separator = '']) {
    return map((e) => '$e$separator').join();
  }
}

extension on String {
  String encloseIfNotEmpty(String open, String close) {
    if (isEmpty) return this;
    return '$open$this$close';
  }
}

extension on DeclaredType {
  List<T> mapTypeParameters<T>(
    T Function(bool isNullable, ReferredType definedType) mapper,
  ) {
    final result = <T>[];
    final offset = classDecl.allTypeParams.length - params.length;
    for (var i = 0; i < classDecl.allTypeParams.length; ++i) {
      final param = i >= offset ? params[i - offset] : DeclaredType.object;
      result.add(mapper(classDecl.allTypeParams[i].isNullable, param));
    }
    return result;
  }
}

String _newLine({int depth = 0}) {
  return '\n${'  ' * depth}';
}

/// Merges two maps. For the same keys, their value lists will be concatenated.
///
/// ** After calling this, the original maps might get modified! **
Map<K, List<V>> _mergeMapValues<K, V>(Map<K, List<V>> a, Map<K, List<V>> b) {
  final merged = <K, List<V>>{};
  for (final key in {...a.keys, ...b.keys}) {
    if (!a.containsKey(key)) {
      merged[key] = b[key]!;
      continue;
    }
    if (!b.containsKey(key)) {
      merged[key] = a[key]!;
      continue;
    }

    // Merging the smaller one to the bigger one
    if (a[key]!.length > b[key]!.length) {
      merged[key] = a[key]!;
      merged[key]!.addAll(b[key]!);
    } else {
      merged[key] = b[key]!;
      merged[key]!.addAll(a[key]!);
    }
  }
  return merged;
}

/// **Naming Convention**
///
/// Let's take the following code as an example:
///
/// ```dart
/// Method definition
/// void f<T extends num, U>(JType<T> $T, JType<U> $U, T t, U u) {
///   // ...
/// }
/// f<int, String>($T, $U, t, u); // Calling the Method
/// ```
///
/// Here `f` will be replaced according to the place of usage.
///
/// * `fArgsDef` refers to `T t, U u` – the arguments in the method
///   definition.
/// * `fArgsCall` refer to `t, u` – the arguments passed to call the method.
/// * `fTypeParamsDef` refers to `<T extends num, U>` – the type parameters
///   of the method at the point of definition.
/// * `fTypeParamsCall` refers to `<int, String>` – the type parameters when
///   calling, or whenever we don't want to have the `extends` keyword.
/// * `fTypeClassesDef` refers to `JType<T> $T, JType<U> $U`.
/// * `fTypeClassesCall` refers to `$T, $U` when calling the method.
class DartGenerator extends Visitor<Classes, Future<void>> {
  final Config config;

  DartGenerator(this.config);

  // Do not change this.
  static const autoGeneratedNotice = '// AUTO GENERATED BY JNIGEN $version. '
      'DO NOT EDIT!\n';
  static const defaultImports = '''
import 'dart:core' show Object, String, bool, double, int;
import 'dart:core' as $_core;

import 'package:jni/_internal.dart' as $_jni;
import 'package:jni/jni.dart' as $_jni;

''';

  // Sort alphabetically.
  static const defaultLintSuppressions = '''
// ignore_for_file: annotate_overrides
// ignore_for_file: argument_type_not_assignable
// ignore_for_file: camel_case_extensions
// ignore_for_file: camel_case_types
// ignore_for_file: constant_identifier_names
// ignore_for_file: comment_references
// ignore_for_file: doc_directive_unknown
// ignore_for_file: file_names
// ignore_for_file: inference_failure_on_untyped_parameter
// ignore_for_file: invalid_internal_annotation
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: library_prefixes
// ignore_for_file: lines_longer_than_80_chars
// ignore_for_file: no_leading_underscores_for_library_prefixes
// ignore_for_file: no_leading_underscores_for_local_identifiers
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: only_throw_errors
// ignore_for_file: overridden_fields
// ignore_for_file: prefer_double_quotes
// ignore_for_file: unintended_html_in_doc_comment
// ignore_for_file: unnecessary_cast
// ignore_for_file: unnecessary_non_null_assertion
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: unused_element
// ignore_for_file: unused_field
// ignore_for_file: unused_import
// ignore_for_file: unused_local_variable
// ignore_for_file: unused_shown_name
// ignore_for_file: use_super_parameters

''';

  /// Run dart format command on [path].
  Future<void> _runDartFormat(String path) async {
    log.info('Running dart format...');
    final formatRes = await Process.run('dart', ['format', path]);
    // if negative exit code, likely due to an interrupt.
    if (formatRes.exitCode > 0) {
      log.fatal(
        'Dart format completed with exit code ${formatRes.exitCode} '
        'This usually means there\'s a syntax error in bindings.\n'
        'Please look at the generated files and report a bug: \n'
        'https://github.com/dart-lang/native/issues/new?labels=package%3Ajnigen\n',
      );
    }
  }

  @override
  Future<void> visit(Classes node) async {
    final root = config.outputConfig.dartConfig.path;
    final preamble = config.preamble ?? '';
    if (config.outputConfig.dartConfig.structure ==
        OutputStructure.singleFile) {
      final file = File.fromUri(root);
      await file.create(recursive: true);
      log.info('Generating bindings');
      final s = file.openWrite();
      s.writeln(autoGeneratedNotice);
      s.writeln(preamble);
      s.writeln(defaultLintSuppressions);
      s.writeln(defaultImports);
      final resolver = Resolver(
        importedClasses: config.importedClasses,
        currentClass: null, // Single file mode.
        inputClassNames: node.decls.keys.toSet(),
      );
      final classGenerator = _ClassGenerator(config, s, resolver);
      for (final classDecl in node.decls.values) {
        classDecl.accept(classGenerator);
      }
      await s.close();
      await _runDartFormat(file.path);
      return;
    }
    final files = <String, List<ClassDecl>>{};
    final packages = <String, Set<String>>{};
    for (final classDecl in node.decls.values) {
      final fileClass = Resolver.getFileClassName(classDecl.binaryName);

      files.putIfAbsent(fileClass, () => <ClassDecl>[]);
      files[fileClass]!.add(classDecl);

      packages.putIfAbsent(classDecl.packageName, () => {});
      packages[classDecl.packageName]!.add(fileClass.split('.').last);
    }

    log.info('Using dart root = $root');
    {
      final rootDirectory = Directory.fromUri(root);
      if (rootDirectory.existsSync()) {
        final files = rootDirectory.listSync(recursive: true).whereType<File>();
        final filesToRemove = <File>[];
        for (final file in files) {
          final stream = file.openRead();
          final firstLine = await stream
              .transform(const Utf8Decoder())
              .transform(const LineSplitter())
              .first;
          if (firstLine.startsWith('// AUTO GENERATED BY JNIGEN ')) {
            log.fine('Removing old file ${file.path}');
            filesToRemove.add(file);
          } else {
            throw StateError('''
A file that was not generated by JNIgen was found in ${root.path}: ${file.path}.''');
          }
        }
        await filesToRemove.map((file) => file.delete()).wait;
      }
    }
    for (var fileClassName in files.keys) {
      final relativeFileName = '${fileClassName.replaceAll('.', '/')}.dart';
      final dartFileUri = root.resolve(relativeFileName);
      final dartFile = await File.fromUri(dartFileUri).create(recursive: true);
      log.fine('$fileClassName -> ${dartFile.path}');

      final classesInFile = files[fileClassName]!;
      final dartFileStream = dartFile.openWrite();
      dartFileStream.writeln(autoGeneratedNotice);
      dartFileStream.writeln(preamble);
      dartFileStream.writeln(defaultLintSuppressions);
      dartFileStream.writeln(defaultImports);
      final s = StringBuffer();
      final resolver = Resolver(
        importedClasses: config.importedClasses,
        currentClass: fileClassName,
        inputClassNames: node.decls.keys.toSet(),
      );
      final classGenerator = _ClassGenerator(config, s, resolver);
      for (final classDecl in classesInFile) {
        classDecl.accept(classGenerator);
      }
      dartFileStream.writeAll(resolver.importStrings, '\n');
      dartFileStream.writeln(s.toString());
      await dartFileStream.close();
    }

    // write _package.dart export files
    for (var package in packages.keys) {
      final dirUri = root.resolve('${package.replaceAll('.', '/')}/');
      final exportFileUri = dirUri.resolve('_package.dart');
      final exportFile = File.fromUri(exportFileUri);
      exportFile.createSync(recursive: true);
      final exports =
          packages[package]!.map((cls) => "export '$cls.dart';").join('\n');
      exportFile.writeAsStringSync(autoGeneratedNotice + exports);
    }
    await _runDartFormat(root.toFilePath());
    log.info('Completed.');
  }
}

/// Generates the Dart class definition, type class, and the array extension.
class _ClassGenerator extends Visitor<ClassDecl, void> {
  final Config config;
  final StringSink s;
  final Resolver resolver;

  _ClassGenerator(this.config, this.s, this.resolver);

  static const staticTypeGetter = 'type';
  static const instanceTypeGetter = '\$$staticTypeGetter';

  void generateFieldsAndMethods(ClassDecl node, String classRef) {
    final fieldGenerator = _FieldGenerator(
      config,
      resolver,
      s,
      isTopLevel: node.isTopLevel,
      classRef: classRef,
    );
    for (final field in node.fields) {
      field.accept(fieldGenerator);
    }
    final methodGenerator = _MethodGenerator(
      config,
      resolver,
      s,
      isTopLevel: node.isTopLevel,
      classRef: classRef,
    );
    for (final method in node.methods) {
      method.accept(methodGenerator);
    }
  }

  String writeClassRef(ClassDecl node) {
    final internalName = node.internalName;
    final modifier = node.isTopLevel ? '' : '  static ';
    final classRef = node.isTopLevel ? '_${node.finalName}Class' : '_class';
    s.write('''
${modifier}final $classRef = $_jni.JClass.forName(r'$internalName');

    ''');
    return classRef;
  }

  @override
  void visit(ClassDecl node) {
    if (node.isTopLevel) {
      // If the class is top-level, only generate its methods and fields.
      final classRef = writeClassRef(node);
      generateFieldsAndMethods(node, classRef);
      return;
    }
    // Docs.
    s.write('/// from: `${node.binaryName}`\n');
    node.javadoc?.accept(_DocGenerator(s, depth: 0));

    // Class definition.
    final name = node.finalName;
    final superName = node.superclass!.accept(
      _TypeGenerator(resolver, includeNullability: false),
    );
    final implClassName = '\$$name';
    final typeParamsDef = node.allTypeParams
        .accept(const _TypeParamDef())
        .join(', ')
        .encloseIfNotEmpty('<', '>');
    final typeParams = node.allTypeParams.map((typeParam) => typeParam.name);
    final typeParamsCall = typeParams
        .map((typeParam) => '$_typeParamPrefix$typeParam')
        .join(', ')
        .encloseIfNotEmpty('<', '>');
    final staticTypeGetterCallArgs =
        typeParams.join(', ').encloseIfNotEmpty('(', ')');
    final typeClassesDef = typeParams
        .map(
          (typeParam) => '''
  $_internal
  final $_jType<$_typeParamPrefix$typeParam> $typeParam;
''',
        )
        .join('\n');
    final ctorTypeClassesDef = typeParams
        .map((typeParam) => 'this.$typeParam,')
        .join(_newLine(depth: 2));
    final superClass = node.classDecl.superclass! as DeclaredType;
    final superTypeClassesCall = superClass.classDecl.isObject
        ? ''
        : superClass.params
            .accept(_TypeClassGenerator(resolver))
            .map((typeClass) => '${typeClass.name},')
            .join(_newLine(depth: 2));
    s.write('''
class $name$typeParamsDef extends $superName {
  $_internal
  $_override
  final $_jType<$name$typeParamsCall> $instanceTypeGetter;

  $typeClassesDef

  $_internal
  $name.fromReference(
    $ctorTypeClassesDef
    $_jReference reference,
  ) :
    $instanceTypeGetter = $staticTypeGetter$typeParamsCall$staticTypeGetterCallArgs,
    super.fromReference(
      $superTypeClassesCall
      reference
    );

''');

    final classRef = writeClassRef(node);

    // Static TypeClass getter.
    s.writeln(
      '  /// The type which includes information such as the signature of this class.',
    );
    void generateTypeClassGetter({required bool isNullable}) {
      final typeClassName =
          isNullable ? node.nullableTypeClassName : node.typeClassName;
      final typeClassGetterName =
          isNullable ? 'nullableType' : staticTypeGetter;
      if (typeParams.isEmpty) {
        s.writeln(
          'static const $typeClassGetterName = '
          '$typeClassName$typeParamsCall();',
        );
      } else {
        final staticTypeGetterTypeClassesDef = typeParams
            .map(
              (typeParam) => '$_jType<$_typeParamPrefix$typeParam> $typeParam,',
            )
            .join(_newLine(depth: 2));
        final typeClassesCall = typeParams
            .map((typeParam) => '$typeParam,')
            .join(_newLine(depth: 3));
        s.write('''
  static $typeClassName$typeParamsCall $typeClassGetterName$typeParamsDef(
    $staticTypeGetterTypeClassesDef
  ) {
    return $typeClassName$typeParamsCall(
      $typeClassesCall
    );
  }

''');
      }
    }

    generateTypeClassGetter(isNullable: true);
    generateTypeClassGetter(isNullable: false);

    // Fields and Methods
    generateFieldsAndMethods(node, classRef);

    // Operators
    for (final MapEntry(key: operator, value: method)
        in node.operators.entries) {
      method.accept(_OperatorGenerator(resolver, s, operator: operator));
    }
    node.compareTo?.accept(_ComparatorGenerator(resolver, s));

    if (node.declKind == DeclKind.interfaceKind) {
      s.write('''
  /// Maps a specific port to the implemented interface.
  static final $_core.Map<int, $implClassName> _\$impls = {};
''');
      s.write('''
  static $_jni.JObjectPtr _\$invoke(
    int port,
    $_jni.JObjectPtr descriptor,
    $_jni.JObjectPtr args,
  ) {
    return _\$invokeMethod(
      port,
      $_methodInvocation.fromAddresses(
        0,
        descriptor.address,
        args.address,
      ),
    );
  }

  static final $_jni.Pointer<
          $_jni.NativeFunction<
              $_jni.JObjectPtr Function(
                  $_jni.Int64, $_jni.JObjectPtr, $_jni.JObjectPtr)>>
      _\$invokePointer = $_jni.Pointer.fromFunction(_\$invoke);

  static $_jni.Pointer<$_jni.Void> _\$invokeMethod(
    int \$p,
    $_methodInvocation \$i,
  ) {
    try {
      final \$d = \$i.methodDescriptor.toDartString(releaseOriginal: true);
      final \$a = \$i.args;
    ''');
      final proxyMethodIf = _InterfaceMethodIf(resolver, s);
      for (final method in node.methods) {
        method.accept(proxyMethodIf);
      }
      s.write('''
    } catch (e) {
      return $_protectedExtension.newDartException(e);
    }
    return $_jni.nullptr;
  }

  static void implementIn$typeParamsDef(
    $_jni.JImplementer implementer,
    $implClassName$typeParamsCall \$impl,
  ) {
    late final $_jni.RawReceivePort \$p;
    \$p = $_jni.RawReceivePort((\$m) {
      if (\$m == null) {
        _\$impls.remove(\$p.sendPort.nativePort);
        \$p.close();
        return;
      }
      final \$i = $_methodInvocation.fromMessage(\$m);
      final \$r = _\$invokeMethod(\$p.sendPort.nativePort, \$i);
      $_protectedExtension.returnResult(\$i.result, \$r);
    });
    implementer.add(
      r'${node.binaryName}',
      \$p,
      _\$invokePointer,
      [
''');
      final interfaceAsyncMethod = _InterfaceIfAsyncMethod(
        resolver,
        s,
        implClassName: implClassName,
      );
      for (final method in node.methods) {
        method.accept(interfaceAsyncMethod);
      }
      s.write('''
      ],
    );
    final \$a = \$p.sendPort.nativePort; 
    _\$impls[\$a] = \$impl;
  }

  factory $name.implement(
    $implClassName$typeParamsCall \$impl,
  ) {
''');
      final typeClassesCall = typeParams
          .map((typeParam) => '\$impl.$typeParam,')
          .join(_newLine(depth: 3));
      s.write('''
    final \$i = $_jni.JImplementer();
    implementIn(\$i, \$impl);
    return $name$typeParamsCall.fromReference(
      $typeClassesCall
      \$i.implementReference(),
    );
  }
  ''');
    }

    // Writing any custom code provided for this class.
    if (config.customClassBody?.containsKey(node.binaryName) ?? false) {
      s.writeln(config.customClassBody![node.binaryName]);
    }

    // End of Class definition.
    s.writeln('}');

    // Abstract and concrete Impl class definition.
    // Used for interface implementation.
    if (node.declKind == DeclKind.interfaceKind) {
      // Abstract Impl class.
      final typeClassGetters = typeParams
          .map(
            (typeParam) =>
                '$_jType<$_typeParamPrefix$typeParam> get $typeParam;',
          )
          .join(_newLine(depth: 1));
      final abstractFactoryArgs = [
        ...typeParams.map(
          (typeParam) => 'required $_jType<\$$typeParam> $typeParam,',
        ),
        ...node.methods.accept(_AbstractImplFactoryArg(resolver)),
      ].join(_newLine(depth: 2)).encloseIfNotEmpty('{', '}');
      s.write('''
abstract base mixin class $implClassName$typeParamsDef {
  factory $implClassName(
    $abstractFactoryArgs
  ) = _$implClassName$typeParamsCall;

  $typeClassGetters

''');
      final abstractImplMethod = _AbstractImplMethod(resolver, s);
      for (final method in node.methods) {
        method.accept(abstractImplMethod);
      }
      s.writeln('}');

      // Concrete Impl class.
      // This is for passing closures instead of implementing the class.
      final concreteCtorArgs = [
        ...typeParams.map((typeParam) => 'required this.$typeParam,'),
        ...node.methods.accept(_ConcreteImplClosureCtorArg(resolver)),
      ].join(_newLine(depth: 2)).encloseIfNotEmpty('{', '}');
      final setClosures = node.methods
          .map((method) => '_${method.finalName} = ${method.finalName}')
          .join(', ')
          .encloseIfNotEmpty(' :  ', '');
      final typeClassesDef = typeParams
          .map(
            (typeParam) => '''
$_override
final $_jType<\$$typeParam> $typeParam;
''',
          )
          .join(_newLine(depth: 1));
      s.write('''

final class _$implClassName$typeParamsDef with $implClassName$typeParamsCall {
  _$implClassName(
    $concreteCtorArgs
  )$setClosures;

  $typeClassesDef

''');
      final concreteClosureDef = _ConcreteImplClosureDef(resolver, s);
      for (final method in node.methods) {
        method.accept(concreteClosureDef);
      }
      s.writeln();
      final concreteMethodDef = _ConcreteImplMethod(resolver, s);
      for (final method in node.methods) {
        method.accept(concreteMethodDef);
      }
      s.writeln('}');
    }
    // TypeClass definition.
    void generateTypeClass({required bool isNullable}) {
      final typeClassName =
          isNullable ? node.nullableTypeClassName : node.typeClassName;
      final typeClassesCall =
          typeParams.map((typeParam) => '$typeParam,').join(_newLine(depth: 2));
      final signature = node.signature;
      final superType = superClass.accept(_TypeClassGenerator(resolver)).name;
      final hashCodeTypeClasses = typeParams.join(', ');
      final equalityTypeClasses = typeParams
          .map((typeParam) => ' &&\n        $typeParam == other.$typeParam')
          .join();
      final hashCode = typeParams.isEmpty
          ? '($typeClassName).hashCode'
          : 'Object.hash($typeClassName, $hashCodeTypeClasses)';
      final nullableType = isNullable
          ? 'this'
          : (DeclaredType(
              binaryName: node.binaryName,
              annotations: [Annotation.nullable],
              params: node.allTypeParams
                  .map(
                    (typeParam) => TypeVar(name: typeParam.name)
                      ..origin = TypeParam(
                        name: typeParam.name,
                        annotations: [Annotation.nonNull],
                        bounds: typeParam.bounds,
                      ),
                  )
                  .toList(),
            )..classDecl = node)
              .accept(_TypeClassGenerator(resolver))
              .name;
      final nullable = isNullable ? '?' : '';
      s.write('''
final class $typeClassName$typeParamsDef extends $_jType<$name$typeParamsCall$nullable> {
  $typeClassesDef

  $_internal
  const $typeClassName(
    $ctorTypeClassesDef
  );

  $_internal
  $_override
  String get signature => r'$signature';

  $_internal
  $_override
  $name$typeParamsCall$nullable fromReference($_jReference reference) =>
  ''');
      if (isNullable) {
        s.write('''
    reference.isNull ? null : $name$typeParamsCall.fromReference(
      $typeClassesCall
      reference,
    );
''');
      } else {
        s.write('''
    $name$typeParamsCall.fromReference(
      $typeClassesCall
      reference,
    );
''');
      }
      s.write('''
  $_internal
  $_override
  $_jType get superType => $superType;

  $_internal
  $_override
  $_jType<$name$typeParamsCall?> get nullableType => $nullableType;

  $_internal
  $_override
  final superCount = ${node.superCount};

  $_override
  int get hashCode => $hashCode;

  $_override
  bool operator ==(Object other) {
    return other.runtimeType == ($typeClassName$typeParamsCall) &&
        other is $typeClassName$typeParamsCall$equalityTypeClasses;
  }
}

''');
    }

    generateTypeClass(isNullable: true);
    generateTypeClass(isNullable: false);

    log.finest('Generated bindings for class ${node.binaryName}');
  }
}

/// Generates the JavaDoc comments.
class _DocGenerator extends Visitor<JavaDocComment, void> {
  final StringSink s;
  final int depth;

  const _DocGenerator(this.s, {required this.depth});

  @override
  void visit(JavaDocComment node) {
    final link = RegExp('{@link ([^{}]+)}');
    final indent = '  ' * depth;
    final comments = node.comment
        .replaceAllMapped(link, (match) => match.group(1) ?? '')
        .replaceAll('#', '\\#')
        .replaceAll('<p>', '')
        .replaceAll('</p>', '\n')
        .replaceAll('<b>', '__')
        .replaceAll('</b>', '__')
        .replaceAll('<em>', '_')
        .replaceAll('</em>', '_')
        .split('\n')
        .join('\n$indent///');
    s.write('''
$indent///
$indent/// $comments
''');
  }
}

/// Generates the user-facing Dart type.
class _TypeGenerator extends TypeVisitor<String> {
  final Resolver? resolver;

  /// Whether the top-type of the current type being visited is nullable.
  ///
  /// For example the top-type of `T` in `Foo<T extends Bar>` is `Bar`, this
  /// will be true if `Bar` is nullable.
  final bool isTopTypeNullable;

  final bool forInterfaceImplementation;

  /// Whether the generic types should be erased.
  final bool typeErasure;

  final bool includeNullability;

  /// Whether the type is used inside `JArray`.
  final bool arrayType;

  const _TypeGenerator(
    this.resolver, {
    this.forInterfaceImplementation = false,
    this.typeErasure = false,
    this.includeNullability = true,
    this.arrayType = false,
    this.isTopTypeNullable = true,
  });

  @override
  String visitArrayType(ArrayType node) {
    final innerType = node.elementType;
    final nullable =
        includeNullability && node.isNullable && isTopTypeNullable ? '?' : '';
    final typeGenerator = _TypeGenerator(
      resolver,
      forInterfaceImplementation: forInterfaceImplementation,
      typeErasure: forInterfaceImplementation,
      includeNullability: true,
      arrayType: true,
      isTopTypeNullable: true,
    );
    if (innerType is PrimitiveType) {
      return '$_jni.J${innerType.accept(typeGenerator)}Array$nullable';
    }
    return '$_jArray<${innerType.accept(typeGenerator)}>$nullable';
  }

  @override
  String visitDeclaredType(DeclaredType node) {
    if (node.classDecl.isObject) {
      // The class is not generated, fall back to `JObject`.
      return super.visitDeclaredType(node);
    }
    final nullable =
        includeNullability && node.isNullable && isTopTypeNullable ? '?' : '';

    final allTypeParams = node.mapTypeParameters(
      (isNullable, definedType) {
        return definedType.accept(_TypeGenerator(
          resolver,
          forInterfaceImplementation: forInterfaceImplementation,
          typeErasure: forInterfaceImplementation,
          includeNullability: true,
          arrayType: false,
          isTopTypeNullable: isNullable,
        ));
      },
    );

    final typeParams = allTypeParams.join(', ').encloseIfNotEmpty('<', '>');
    final prefix = resolver?.resolvePrefix(node.classDecl) ?? '';
    return '$prefix${node.classDecl.finalName}$typeParams$nullable';
  }

  @override
  String visitPrimitiveType(PrimitiveType node) {
    if (arrayType) {
      return node.name.capitalize();
    }
    return node.dartType;
  }

  @override
  String visitTypeVar(TypeVar node) {
    // TODO(https://github.com/dart-lang/native/issues/704): Tighten to
    // typevar bounds instead.
    {
      final nullable =
          includeNullability && node.isNullable && isTopTypeNullable ? '?' : '';
      if (typeErasure) {
        return '$_jObject$nullable';
      }
      if (forInterfaceImplementation && node.origin.parent is Method) {
        return '$_jObject$nullable';
      }
    }
    final nullable = includeNullability && node.hasQuestionMark ? '?' : '';
    return '$_typeParamPrefix${node.name}$nullable';
  }

  @override
  String visitWildcard(Wildcard node) {
    // TODO(https://github.com/dart-lang/native/issues/701): Support wildcards.
    if (node.superBound != null || node.extendsBound == null) {
      // Dart does not support `* super T` wildcards. Fall back to Object?.
      return super.visitWildcard(node);
    }
    final typeGenerator = _TypeGenerator(
      resolver,
      arrayType: arrayType,
      forInterfaceImplementation: forInterfaceImplementation,
      includeNullability:
          includeNullability && node.isNullable && isTopTypeNullable,
      typeErasure: typeErasure,
      isTopTypeNullable: true,
    );
    return node.extendsBound!.accept(typeGenerator);
  }

  @override
  String visitNonPrimitiveType(ReferredType node) {
    final nullable =
        includeNullability && isTopTypeNullable && node.isNullable ? '?' : '';
    return '$_jObject$nullable';
  }
}

class _TypeClass {
  final String name;
  final bool canBeConst;

  const _TypeClass(this.name, this.canBeConst);
}

/// Generates the type class.
class _TypeClassGenerator extends TypeVisitor<_TypeClass> {
  final bool isConst;

  /// Whether the top-type of the current type being visited is nullable.
  ///
  /// For example the top-type of `T` in `Foo<T extends Bar>` is `Bar`, this
  /// will be true if `Bar` is nullable.
  final bool isTopTypeNullable;

  /// Whether or not to return the equivalent boxed type class for primitives.
  /// Only for interface implemetation.
  final bool boxPrimitives;

  /// Whether or not to find the correct type variable from the static map.
  /// Only for interface implemetation.
  final bool forInterfaceImplementation;

  /// Whether the generic types should be erased.
  final bool typeErasure;

  final bool includeNullability;

  final Resolver resolver;

  _TypeClassGenerator(
    this.resolver, {
    this.isConst = true,
    this.boxPrimitives = false,
    this.forInterfaceImplementation = false,
    this.includeNullability = true,
    this.typeErasure = false,
    this.isTopTypeNullable = true,
  });

  @override
  _TypeClass visitArrayType(ArrayType node) {
    final innerTypeClass = node.elementType.accept(
      _TypeClassGenerator(
        resolver,
        isConst: false,
        boxPrimitives: false,
        forInterfaceImplementation: forInterfaceImplementation,
        // Do type erasure for interface implementation.
        typeErasure: forInterfaceImplementation,
        isTopTypeNullable: true,
      ),
    );
    final innerType = node.elementType.accept(
      _TypeGenerator(
        resolver,
        forInterfaceImplementation: forInterfaceImplementation,
        // Do type erasure for interface implementation.
        typeErasure: forInterfaceImplementation,
        arrayType: true,
        isTopTypeNullable: true,
      ),
    );
    final ifConst = innerTypeClass.canBeConst && isConst ? 'const ' : '';
    final type = includeNullability && node.isNullable && isTopTypeNullable
        ? 'NullableType'
        : 'Type';
    if (node.elementType is PrimitiveType) {
      return _TypeClass(
        '$ifConst$_jni.J${innerType}Array$type()',
        innerTypeClass.canBeConst,
      );
    }
    return _TypeClass(
      '$ifConst$_jArray$type<$innerType>(${innerTypeClass.name})',
      innerTypeClass.canBeConst,
    );
  }

  @override
  _TypeClass visitDeclaredType(DeclaredType node) {
    if (node.classDecl.isObject) {
      // The class is not generated, fall back to `JObject`.
      return super.visitDeclaredType(node);
    }
    final allTypeClasses = node.mapTypeParameters(
      (isNullable, definedType) {
        return definedType.accept(_TypeClassGenerator(
          resolver,
          isConst: false,
          boxPrimitives: false,
          forInterfaceImplementation: forInterfaceImplementation,
          typeErasure: forInterfaceImplementation,
          isTopTypeNullable: isNullable,
        ));
      },
    );

    // Can be const if all the type parameters are defined and each of them are
    // also const.
    final canBeConst = allTypeClasses.every((e) => e.canBeConst);

    // Add const to subexpressions if the entire expression is not const.
    final allTypeParams = allTypeClasses
        .map((typeClass) =>
            '${typeClass.canBeConst && !canBeConst ? 'const ' : ''}'
            '${typeClass.name}')
        .toList();

    final args = allTypeParams.join(', ');
    final ifConst = isConst && canBeConst ? 'const ' : '';
    final type = includeNullability && node.isNullable && isTopTypeNullable
        ? node.classDecl.nullableTypeClassName
        : node.classDecl.typeClassName;

    final typeArgsList = node.mapTypeParameters(
      (isNullable, definedType) {
        return definedType.accept(
          _TypeGenerator(
            resolver,
            forInterfaceImplementation: forInterfaceImplementation,
            // Do type erasure for interface implementation.
            typeErasure: forInterfaceImplementation,
            isTopTypeNullable: isNullable,
          ),
        );
      },
    );
    final typeArgs = typeArgsList.join(', ').encloseIfNotEmpty('<', '>');
    final prefix = resolver.resolvePrefix(node.classDecl);
    return _TypeClass('$ifConst$prefix$type$typeArgs($args)', canBeConst);
  }

  @override
  _TypeClass visitPrimitiveType(PrimitiveType node) {
    final ifConst = isConst ? 'const ' : '';
    final name = boxPrimitives ? 'J${node.boxedName}' : 'j${node.name}';
    return _TypeClass('$ifConst$_jni.${name}Type()', true);
  }

  @override
  _TypeClass visitTypeVar(TypeVar node) {
    // TODO(https://github.com/dart-lang/native/issues/704): Tighten to typevar
    // bounds instead.
    final type = includeNullability && node.hasQuestionMark && isTopTypeNullable
        ? 'NullableType'
        : 'Type';
    final convertToNullable =
        includeNullability && node.hasQuestionMark && isTopTypeNullable
            ? '.nullableType'
            : '';
    if (typeErasure) {
      final ifConst = isConst ? 'const ' : '';
      return _TypeClass('$ifConst$_jObject$type()', true);
    }
    if (forInterfaceImplementation) {
      if (node.origin.parent is ClassDecl) {
        return _TypeClass(
          '_\$impls[\$p]!.${node.name}$convertToNullable',
          false,
        );
      }
      final ifConst = isConst ? 'const ' : '';
      return _TypeClass('$ifConst$_jObject$type()', true);
    }
    return _TypeClass('${node.name}$convertToNullable', false);
  }

  @override
  _TypeClass visitWildcard(Wildcard node) {
    // TODO(https://github.com/dart-lang/native/issues/701): Support wildcards.
    if (node.superBound != null || node.extendsBound == null) {
      // Dart does not support `* super T` wildcards. Fall back to Object?.
      return super.visitWildcard(node);
    }
    final typeClassGenerator = _TypeClassGenerator(
      resolver,
      boxPrimitives: boxPrimitives,
      forInterfaceImplementation: forInterfaceImplementation,
      includeNullability:
          includeNullability && node.isNullable && isTopTypeNullable,
      isConst: isConst,
      typeErasure: typeErasure,
      isTopTypeNullable: true,
    );
    return node.extendsBound!.accept(typeClassGenerator);
  }

  @override
  _TypeClass visitNonPrimitiveType(ReferredType node) {
    final ifConst = isConst ? 'const ' : '';
    final type = includeNullability && node.isNullable && isTopTypeNullable
        ? 'NullableType'
        : 'Type';
    return _TypeClass('$ifConst$_jObject$type()', true);
  }
}

class _TypeParamDef extends Visitor<TypeParam, String> {
  const _TypeParamDef();

  @override
  String visit(TypeParam node) {
    // TODO(https://github.com/dart-lang/native/issues/704): resolve the actual
    // type being extended, if any.
    final nullable = node.isNullable ? '?' : '';
    return '$_typeParamPrefix${node.name} extends $_jObject$nullable';
  }
}

class _JniResultGetter extends TypeVisitor<String> {
  final Resolver resolver;

  _JniResultGetter(this.resolver);

  @override
  String visitPrimitiveType(PrimitiveType node) {
    if (node.name == 'void') return 'check()';
    if (node.name == 'double') return 'doubleFloat';
    if (node.name == 'int') return 'integer';
    return node.name;
  }

  @override
  String visitNonPrimitiveType(ReferredType node) {
    final typeClass = node.accept(_TypeClassGenerator(resolver)).name;
    final type = node.accept(_TypeGenerator(resolver));
    return 'object<$type>($typeClass)';
  }
}

/// Type signature for Dart and C's varargs.
///
/// When `isFfi` is `true`, it generates the ffi type signature for vararg.
///
/// For example `ffi.Int32` is an ffi type signature while `int` is a Dart one.
class _TypeSig extends TypeVisitor<String> {
  final bool isFfi;

  const _TypeSig({required this.isFfi});

  @override
  String visitPrimitiveType(PrimitiveType node) {
    if (isFfi) return '$_jni.${node.ffiVarArgType}';
    if (node.name == 'boolean') return 'int';
    return node.dartType;
  }

  @override
  String visitNonPrimitiveType(ReferredType node) {
    return _voidPointer;
  }
}

class _ToNativeSuffix extends TypeVisitor<String> {
  const _ToNativeSuffix();

  @override
  String visitPrimitiveType(PrimitiveType node) {
    if (node.name == 'boolean') {
      return ' ? 1 : 0';
    }
    return '';
  }

  @override
  String visitNonPrimitiveType(ReferredType node) {
    return '.pointer';
  }
}

class _FieldGenerator extends Visitor<Field, void> {
  final Config config;
  final Resolver resolver;
  final StringSink s;
  final bool isTopLevel;
  final String classRef;

  const _FieldGenerator(
    this.config,
    this.resolver,
    this.s, {
    required this.isTopLevel,
    required this.classRef,
  });

  String get modifier => isTopLevel ? '' : '  static ';

  void writeAccessor(Field node) {
    final name = node.finalName;
    final staticOrInstance = node.isStatic ? 'static' : 'instance';
    final descriptor = node.type.descriptor;
    s.write('''
${modifier}final _id_$name =
      $classRef.${staticOrInstance}FieldId(
        r'${node.name}',
        r'$descriptor',
      );
''');
  }

  String dartOnlyGetter(Field node) {
    final name = node.finalName;
    final self = node.isStatic ? classRef : _self;
    final type = node.type.accept(_TypeClassGenerator(resolver)).name;
    return '_id_$name.get($self, $type)';
  }

  String dartOnlySetter(Field node) {
    final name = node.finalName;
    final self = node.isStatic ? classRef : _self;
    final type = node.type.accept(_TypeClassGenerator(resolver)).name;
    return '_id_$name.set($self, $type, value)';
  }

  void writeDocs(Field node, {required bool writeReleaseInstructions}) {
    final originalDecl = '${node.type} ${node.name}';
    s.writeln('  /// from: `${node.modifiers.join(' ')} $originalDecl`');
    if (node.type is! PrimitiveType && writeReleaseInstructions) {
      s.writeln(_releaseInstruction);
    }
    node.javadoc?.accept(_DocGenerator(s, depth: 1));
  }

  @override
  void visit(Field node) {
    // Check if it should be a `static const` getter.
    if (node.isFinal && node.isStatic && node.defaultValue != null) {
      final name = node.finalName;
      final value = node.defaultValue!;
      if (value is num || value is bool) {
        writeDocs(node, writeReleaseInstructions: false);
        s.writeln('${modifier}const $name = $value;');
        return;
      }
    }

    // Accessors.
    writeAccessor(node);

    // Getter docs.
    writeDocs(node, writeReleaseInstructions: true);

    final name = node.finalName;
    final ifStatic = node.isStatic && !isTopLevel ? 'static ' : '';
    final type = node.type.accept(_TypeGenerator(resolver));
    s.write('$ifStatic$type get $name => ');
    s.write(dartOnlyGetter(node));
    s.writeln(';\n');
    if (!node.isFinal) {
      // Setter docs.
      writeDocs(node, writeReleaseInstructions: true);

      s.write('${ifStatic}set $name($type value) => ');
      s.write(dartOnlySetter(node));
      s.writeln(';\n');
    }
  }
}

class _MethodTypeSig extends Visitor<Method, String> {
  final bool isFfi;

  const _MethodTypeSig({required this.isFfi});

  @override
  String visit(Method node) {
    final callParams = node.params
        .map((param) => param.type)
        .accept(_TypeSig(isFfi: isFfi))
        .join(', ');
    final args = [
      _voidPointer,
      '$_jni.JMethodIDPtr',
      isFfi && callParams.isNotEmpty
          ? '$_jni.VarArgs<($callParams${node.params.length == 1 ? ',' : ''})>'
          : callParams,
    ].join(', ');
    final isCtor = node.isConstructor;
    final isVoid = node.returnType.name == 'void';
    final returnType = !isCtor && isVoid ? _jThrowable : _jResult;
    return '$returnType Function($args)';
  }
}

/// Generates Dart bindings for Java methods.
class _MethodGenerator extends Visitor<Method, void> {
  final Config config;
  final Resolver resolver;
  final StringSink s;
  final bool isTopLevel;
  final String classRef;

  const _MethodGenerator(
    this.config,
    this.resolver,
    this.s, {
    required this.isTopLevel,
    required this.classRef,
  });

  String get modifier => isTopLevel ? '' : '  static ';

  void writeAccessor(Method node) {
    final name = node.finalName;
    final kind = node.isConstructor
        ? 'constructor'
        : node.isStatic
            ? 'staticMethod'
            : 'instanceMethod';
    final descriptor = node.descriptor;
    s.write('''
${modifier}final _id_$name = $classRef.${kind}Id(
''');
    if (!node.isConstructor) s.writeln("    r'${node.name}',");
    s.write('''
    r'$descriptor',
  );
''');
    final ffiSig = node.accept(const _MethodTypeSig(isFfi: true));
    final dartSig = node.accept(const _MethodTypeSig(isFfi: false));
    final methodName = node.accept(const _CallMethodName());
    s.write('''

${modifier}final _$name = $_protectedExtension
    .lookup<$_jni.NativeFunction<$ffiSig>>('$methodName')
    .asFunction<$dartSig>();
''');
  }

  bool isSuspendFun(Method node) {
    return node.asyncReturnType != null;
  }

  String constructor(Method node) {
    final name = node.finalName;
    final params = [
      '$classRef.reference.pointer',
      '_id_$name as $_jni.JMethodIDPtr',
      ...node.params.accept(const _ParamCall()),
    ].join(', ');
    return '_$name($params).reference';
  }

  String methodCall(Method node) {
    final name = node.finalName;
    final params = [
      node.isStatic ? '$classRef.reference.pointer' : 'reference.pointer',
      '_id_$name as $_jni.JMethodIDPtr',
      ...node.params.accept(const _ParamCall()),
    ].join(', ');
    final resultGetter = node.returnType.accept(_JniResultGetter(resolver));
    return '_$name($params).$resultGetter';
  }

  @override
  void visit(Method node) {
    // Accessors
    writeAccessor(node);

    // Docs
    s.write('  /// from: `');
    s.writeAll(node.modifiers.map((m) => '$m '));
    s.write('${node.returnType} ${node.name}(');
    s.writeAll(node.params.map((p) => '${p.type} ${p.name}'), ', ');
    s.writeln(')`');
    if (node.returnType is! PrimitiveType || node.isConstructor) {
      s.writeln(_releaseInstruction);
    }
    node.javadoc?.accept(_DocGenerator(s, depth: 1));

    // Used for inferring the type parameter from the given parameters.
    final typeLocators = node.params
        .accept(_ParamTypeLocator(resolver: resolver))
        .fold(<String, List<String>>{}, _mergeMapValues).map(
      (key, value) =>
          MapEntry(key, value.delimited(', ').encloseIfNotEmpty('[', ']')),
    );

    bool isRequired(TypeParam typeParam) {
      return (typeLocators[typeParam.name] ?? '').isEmpty;
    }

    final typeInference =
        (node.isConstructor ? node.classDecl.allTypeParams : node.typeParams)
            .where((tp) => !isRequired(tp))
            .map((tp) => tp.name)
            .map(
              (tp) => '$tp ??= $_jni.lowestCommonSuperType'
                  '(${typeLocators[tp]}) as $_jType<$_typeParamPrefix$tp>;',
            )
            .join(_newLine(depth: 2));
    // This is needed to keep the references alive in the scope while waiting
    // for the FFI call.
    final localReferences = node.params
        .accept(const _ParamReference())
        .where((ref) => ref.isNotEmpty)
        .toList();
    if (node.isConstructor) {
      final className = node.classDecl.finalName;
      final name = node.finalName;
      final ctorName = name == 'new\$' ? className : '$className.$name';
      final paramsDef = node.params.accept(_ParamDef(resolver)).delimited(', ');
      final typeParamsCall = node.classDecl.allTypeParams
          .map((typeParam) => '$_typeParamPrefix${typeParam.name}')
          .join(', ')
          .encloseIfNotEmpty('<', '>');
      final typeClassDef = node.classDecl.allTypeParams
          .map(
            (typeParam) => typeParam.accept(
              _CtorTypeClassDef(isRequired: isRequired(typeParam)),
            ),
          )
          .delimited(', ')
          .encloseIfNotEmpty('{', '}');
      final typeClassCall = node.classDecl.allTypeParams
          .map((typeParam) => typeParam.name)
          .delimited(', ');

      final ctorExpr = constructor(node);
      s.write('''
  factory $ctorName($paramsDef$typeClassDef) {
    $typeInference
    ${localReferences.join(_newLine(depth: 2))}
    return ${node.classDecl.finalName}$typeParamsCall.fromReference(
      $typeClassCall
      $ctorExpr
    );
  }

''');
      return;
    }

    final name = node.finalName;
    final returnType = isSuspendFun(node)
        ? '$_core.Future<'
            '${node.asyncReturnType!.accept(_TypeGenerator(resolver))}>'
        : node.returnType.accept(_TypeGenerator(resolver));
    final ifStatic = node.isStatic && !isTopLevel ? 'static ' : '';
    final defArgs = node.params.accept(_ParamDef(resolver)).toList();
    final typeClassDef = node.typeParams
        .map(
          (typeParam) => typeParam.accept(
            _MethodTypeClassDef(isRequired: isRequired(typeParam)),
          ),
        )
        .delimited(', ')
        .encloseIfNotEmpty('{', '}');
    final typeParamsDef = node.typeParams
        .accept(const _TypeParamDef())
        .join(', ')
        .encloseIfNotEmpty('<', '>');
    if (isSuspendFun(node)) {
      defArgs.removeLast();
      localReferences.removeLast();
    }
    final params = defArgs.delimited(', ');
    s.write('  $ifStatic$returnType $name$typeParamsDef($params$typeClassDef)');
    final callExpr = methodCall(node);
    if (isSuspendFun(node)) {
      final returningType =
          node.asyncReturnType!.accept(_TypeGenerator(resolver));
      final returningTypeClass =
          node.asyncReturnType!.accept(_TypeClassGenerator(resolver)).name;
      final isNullable = node.asyncReturnType!.isNullable;
      final continuation = node.params.last.finalName;
      s.write('''async {
    $typeInference
    final \$p = $_jni.ReceivePort();
    final _\$$continuation = $_protectedExtension.newPortContinuation(\$p);
    ${localReferences.join(_newLine(depth: 2))}
    final \$r = $callExpr;
    _\$$continuation.release();
    final $_jObject${isNullable ? '?' : ''} \$o;
    if (${isNullable ? '\$r != null && ' : ''}\$r.isInstanceOf($_jni.coroutineSingletonsClass)) {
      \$r.release();
      final \$a = await \$p.first;
      \$o = ${isNullable ? '\$a == 0 ? null :' : ''}$_jObject.fromReference(
          $_jGlobalReference($_jPointer.fromAddress(\$a)));
      if (${isNullable ? '\$o != null && ' : ''}\$o.isInstanceOf($_jni.result\$FailureClass)) {
        final \$e =
            $_jni.failureExceptionField.get(\$o, const ${_jObject}Type());
        \$o.release();
        $_jni.Jni.throwException(\$e.reference.toPointer());
      }
    } else {
      \$o = \$r;
    }
    return \$o${isNullable ? '?' : ''}.as<$returningType>(
      $returningTypeClass,
      releaseOriginal: true,
    );
  }

''');
    } else {
      final returning = returnType == 'void' ? callExpr : 'return $callExpr';
      s.writeln('''{
    $typeInference
    ${localReferences.join(_newLine(depth: 2))}
    $returning;
  }
''');
    }
  }
}

/// Generates the method type param definition.
///
/// For example `required JObjType<T> $T` in:
/// ```dart
/// void bar(..., {required JObjType<T> $T}) => ...
/// ```
class _MethodTypeClassDef extends Visitor<TypeParam, String> {
  final bool isRequired;

  const _MethodTypeClassDef({required this.isRequired});

  @override
  String visit(TypeParam node) {
    return '${isRequired ? 'required ' : ''}$_jType'
        '<$_typeParamPrefix${node.name}>${isRequired ? '' : '?'} ${node.name}';
  }
}

/// Generates the class type param definition. Used only in constructors.
///
/// For example `required this.$T` in:
/// ```dart
/// class Foo {
///   final JObjType<T> $T;
///   Foo(..., {required this.$T}) => ...
/// }
/// ```
class _CtorTypeClassDef extends Visitor<TypeParam, String> {
  final bool isRequired;

  const _CtorTypeClassDef({required this.isRequired});

  @override
  String visit(TypeParam node) {
    return '${isRequired ? 'required ' : ''} $_jType'
        '<$_typeParamPrefix${node.name}>${isRequired ? '' : '?'} ${node.name}';
  }
}

/// Method parameter's definition.
///
/// For example `Foo foo` in:
/// ```dart
/// void bar(Foo foo) => ...
/// ```
class _ParamDef extends Visitor<Param, String> {
  final Resolver resolver;
  final bool methodGenericErasure;

  const _ParamDef(this.resolver, {this.methodGenericErasure = false});

  @override
  String visit(Param node) {
    final type = node.type.accept(
      _TypeGenerator(
        resolver,
        forInterfaceImplementation: methodGenericErasure,
      ),
    );
    return '$type ${node.finalName}';
  }
}

/// Method parameter reference as a local variable.
///
/// `JReference`s are finalizable, in order to make sure they will be kept alive
/// in the method's scope they have to be assigned to a local variable.
///
/// For example:
/// ```dart
/// final _foo = foo.reference;
/// ```
class _ParamReference extends Visitor<Param, String> {
  const _ParamReference();

  @override
  String visit(Param node) {
    if (node.type is PrimitiveType) {
      return '';
    }
    final nullable = node.isNullable ? '?' : '';
    final orDefault = node.isNullable ? ' ?? $_jni.jNullReference' : '';
    return 'final _\$${node.finalName} = '
        '${node.finalName}$nullable.reference$orDefault;';
  }
}

/// Method parameter used in calling the native method.
///
/// For example `foo.reference.pointer` in:
/// ```dart
/// void bar(Foo foo) => _bar(foo.reference.pointer);
/// ```
class _ParamCall extends Visitor<Param, String> {
  const _ParamCall();

  @override
  String visit(Param node) {
    final nativeSuffix = node.type.accept(const _ToNativeSuffix());
    final nonPrimitive = node.type is PrimitiveType ? '' : r'_$';
    final paramCall = '$nonPrimitive${node.finalName}$nativeSuffix';
    return paramCall;
  }
}

/// A pair of [StringBuffer]s that can create an expression from the outer layer
/// inwards.
///
/// For example:
/// ```dart
/// final buffer = OutsideInBuffer(); // asterisk (*) is used to show the middle
/// buffer.appendLeft('f('); // f(*
/// buffer.prependRight('x)'); // f(*x)
/// buffer.appendLeft('g('); // f(g(*x)
/// buffer.prependRight('y) + '); // f(g(*y) + x)
/// buffer.toString(); // f(g(y) + x)
/// ```
@visibleForTesting
class OutsideInBuffer {
  final StringBuffer _leftBuffer;
  final StringBuffer _rightBuffer;

  OutsideInBuffer()
      : _leftBuffer = StringBuffer(),
        _rightBuffer = StringBuffer();

  void prependRight(Object? object) {
    final s = object.toString();
    for (var i = 0; i < s.length; ++i) {
      _rightBuffer.write(s[s.length - i - 1]);
    }
  }

  void appendLeft(Object? object) {
    _leftBuffer.write(object);
  }

  @override
  String toString() {
    return _leftBuffer.toString() + _rightBuffer.toString().reversed;
  }
}

/// The ways to locate each type parameter.
///
/// For example in `JArray<JMap<$T, $T>> a`, `T` can be retreived using
/// ```dart
/// ((((a.$type as JArrayType).elementType) as $JMapType).K)
///   as JObjType<$T>
/// ```
/// and
/// ```dart
/// ((((a.$type as JArrayType).elementType) as $JMapType).V)
///   as JObjType<$T>
/// ```
class _ParamTypeLocator extends Visitor<Param, Map<String, List<String>>> {
  final Resolver resolver;

  _ParamTypeLocator({required this.resolver});

  @override
  Map<String, List<String>> visit(Param node) {
    if (node.isNullable) {
      return {};
    }
    return node.type.accept(_TypeVarLocator(resolver: resolver)).map(
          (key, value) => MapEntry(
            key,
            value
                .map(
                  (e) => (e..appendLeft('${node.finalName}.\$type')).toString(),
                )
                .toList(),
          ),
        );
  }
}

class _TypeVarLocator extends TypeVisitor<Map<String, List<OutsideInBuffer>>> {
  final Resolver resolver;

  _TypeVarLocator({required this.resolver});

  @override
  Map<String, List<OutsideInBuffer>> visitNonPrimitiveType(ReferredType node) {
    return {};
  }

  @override
  Map<String, List<OutsideInBuffer>> visitWildcard(Wildcard node) {
    // TODO(https://github.com/dart-lang/native/issues/701): Support wildcards.
    if (node.superBound != null || node.extendsBound == null) {
      // Dart does not support `* super T` wildcards. Fall back to Object?.
      return super.visitWildcard(node);
    }
    return node.extendsBound!.accept(this);
  }

  @override
  Map<String, List<OutsideInBuffer>> visitTypeVar(TypeVar node) {
    return {
      node.name: [OutsideInBuffer()],
    };
  }

  @override
  Map<String, List<OutsideInBuffer>> visitDeclaredType(DeclaredType node) {
    if (node.classDecl.isObject) {
      // The class is not generated, fall back to `JObject`.
      return super.visitDeclaredType(node);
    }
    final offset = node.classDecl.allTypeParams.length - node.params.length;
    final result = <String, List<OutsideInBuffer>>{};
    final prefix = resolver.resolvePrefix(node.classDecl);
    final typeClass = '$prefix${node.classDecl.typeClassName}';
    final typeClassParams = List.filled(
      node.classDecl.allTypeParams.length,
      '$_core.dynamic',
    ).join(', ').encloseIfNotEmpty('<', '>');
    for (var i = 0; i < node.params.length; ++i) {
      final typeParam = node.classDecl.allTypeParams[i + offset].name;
      final exprs = node.params[i].accept(this);
      for (final expr in exprs.entries) {
        for (final buffer in expr.value) {
          buffer.appendLeft('(');
          buffer.prependRight(' as $typeClass$typeClassParams).$typeParam');
          result[expr.key] = (result[expr.key] ?? [])..add(buffer);
        }
      }
    }
    return result;
  }

  @override
  Map<String, List<OutsideInBuffer>> visitArrayType(ArrayType node) {
    final exprs = node.elementType.accept(this);
    for (final e in exprs.values.expand((i) => i)) {
      e.appendLeft('((');
      e.prependRight(' as ${_jArray}Type).elementType as $_jType)');
    }
    return exprs;
  }

  @override
  Map<String, List<OutsideInBuffer>> visitPrimitiveType(PrimitiveType node) {
    return {};
  }
}

/// Method defintion for Impl abstract class used for interface implementation.
class _AbstractImplMethod extends Visitor<Method, void> {
  final Resolver resolver;
  final StringSink s;

  _AbstractImplMethod(this.resolver, this.s);

  @override
  void visit(Method node) {
    final returnType = node.returnType.accept(
      _TypeGenerator(resolver, forInterfaceImplementation: true),
    );
    final name = node.finalName;
    final args = node.params
        .accept(_ParamDef(resolver, methodGenericErasure: true))
        .join(', ');
    s.writeln('  $returnType $name($args);');
    if (returnType == 'void') {
      s.writeln('  bool get $name\$async => false;');
    }
  }
}

/// Closure defintion for concrete Impl class used for interface implementation.
class _ConcreteImplClosureDef extends Visitor<Method, void> {
  final Resolver resolver;
  final StringSink s;

  _ConcreteImplClosureDef(this.resolver, this.s);

  @override
  void visit(Method node) {
    final returnType = node.returnType.accept(
      _TypeGenerator(resolver, forInterfaceImplementation: true),
    );
    final name = node.finalName;
    final args = node.params
        .accept(_ParamDef(resolver, methodGenericErasure: true))
        .join(', ');
    s.writeln('  final $returnType Function($args) _$name;');
    if (returnType == 'void') {
      s.writeln('  final bool $name\$async;');
    }
  }
}

/// Closure argument for the factory of the implementation's abstract class.
/// Used for interface implementation.
class _AbstractImplFactoryArg extends Visitor<Method, String> {
  final Resolver resolver;

  _AbstractImplFactoryArg(this.resolver);

  @override
  String visit(Method node) {
    final returnType = node.returnType.accept(
      _TypeGenerator(resolver, forInterfaceImplementation: true),
    );
    final name = node.finalName;
    final args = node.params
        .accept(_ParamDef(resolver, methodGenericErasure: true))
        .join(', ');
    final functionArg = 'required $returnType Function($args) $name,';
    if (node.returnType.name == 'void') {
      return '$functionArg bool $name\$async,';
    }
    return functionArg;
  }
}

/// Closure argument for concrete Impl class constructor.
/// Used for interface implementation.
class _ConcreteImplClosureCtorArg extends Visitor<Method, String> {
  final Resolver resolver;

  _ConcreteImplClosureCtorArg(this.resolver);

  @override
  String visit(Method node) {
    final returnType = node.returnType.accept(
      _TypeGenerator(resolver, forInterfaceImplementation: true),
    );
    final name = node.finalName;
    final args = node.params
        .accept(_ParamDef(resolver, methodGenericErasure: true))
        .join(', ');
    final functionArg = 'required $returnType Function($args) $name,';
    if (node.returnType.name == 'void') {
      return '$functionArg this.$name\$async = false,';
    }
    return functionArg;
  }
}

/// Method defintion for concrete Impl class used for interface implementation.
class _ConcreteImplMethod extends Visitor<Method, void> {
  final Resolver resolver;
  final StringSink s;

  _ConcreteImplMethod(this.resolver, this.s);

  @override
  void visit(Method node) {
    final returnType = node.returnType.accept(
      _TypeGenerator(resolver, forInterfaceImplementation: true),
    );
    final name = node.finalName;
    final argsDef = node.params
        .accept(_ParamDef(resolver, methodGenericErasure: true))
        .join(', ');
    final argsCall = node.params.map((param) => param.finalName).join(', ');
    s.write('''
  $returnType $name($argsDef) {
    return _$name($argsCall);
  }''');
  }
}

/// The if statement to check which method has been called from the proxy class.
class _InterfaceMethodIf extends Visitor<Method, void> {
  final Resolver resolver;
  final StringSink s;

  _InterfaceMethodIf(this.resolver, this.s);

  @override
  void visit(Method node) {
    final isVoid = node.returnType.name == 'void';
    final signature = node.javaSig;
    final saveResult = isVoid ? '' : 'final \$r = ';
    final name = node.finalName;
    s.write('''
        if (\$d == r'$signature') {
          ${saveResult}_\$impls[\$p]!.$name(
''');
    for (var i = 0; i < node.params.length; ++i) {
      node.params[i].accept(_InterfaceParamCast(resolver, s, paramIndex: i));
    }
    const returnBox = _InterfaceReturnBox();
    s.write('''
          );
          return ${node.returnType.accept(returnBox)};
        }
''');
  }
}

/// The if statement within the async methods list to conditionally add methods.
class _InterfaceIfAsyncMethod extends Visitor<Method, void> {
  final Resolver resolver;
  final StringSink s;
  final String implClassName;

  _InterfaceIfAsyncMethod(this.resolver, this.s, {required this.implClassName});

  @override
  void visit(Method node) {
    if (node.returnType.name != 'void') {
      return;
    }
    final signature = node.javaSig;
    final name = node.finalName;
    s.write('''
        if (\$impl.$name\$async) r'$signature',
''');
  }
}

/// Generates casting to the correct parameter type from the list of JObject
/// arguments received from the call to the proxy class.
class _InterfaceParamCast extends Visitor<Param, void> {
  final Resolver resolver;
  final StringSink s;
  final int paramIndex;

  _InterfaceParamCast(this.resolver, this.s, {required this.paramIndex});

  @override
  void visit(Param node) {
    final typeClass = node.type
        .accept(
          _TypeClassGenerator(
            resolver,
            boxPrimitives: true,
            forInterfaceImplementation: true,
            includeNullability: false,
          ),
        )
        .name;
    final nullable = node.isNullable && node.type is! PrimitiveType ? '?' : '!';
    s.write('\$a![$paramIndex]$nullable.as($typeClass, releaseOriginal: true)');
    if (node.type is PrimitiveType) {
      // Convert to Dart type.
      final name = node.type.name;
      s.write('.${name}Value(releaseOriginal: true)');
    }
    s.writeln(',');
  }
}

/// Boxes the returned primitive value into the correct Boxed type.
/// Only returns the reference for non primitive types.
/// Returns null for void.
///
/// Since Dart doesn't know that this global reference is still used, it might
/// garbage collect it via `NativeFinalizer` thus making it invalid.
/// This passes the ownership to Java using `toPointer()`.
///
/// `toPointer` detaches the object from the `NativeFinalizer` and Java
/// will clean up the global reference afterwards.
///
/// For example `$r.toJInteger().reference.toPointer()` when the return
/// type is `integer`.
class _InterfaceReturnBox extends TypeVisitor<String> {
  const _InterfaceReturnBox();

  @override
  String visitNonPrimitiveType(ReferredType node) {
    // Casting is done to create a new global reference. The user might
    // use the original reference elsewhere and so the original object
    // should not be `setAsReleased`.
    return '(\$r as $_jObject?)?.as(const ${_jObject}Type())'
        '.reference.toPointer() ?? $_jni.nullptr';
  }

  @override
  String visitPrimitiveType(PrimitiveType node) {
    if (node.name == 'void') {
      return '$_jni.nullptr';
    }
    return '$_jni.J${node.boxedName}(\$r).reference.toPointer()';
  }
}

class _CallMethodName extends Visitor<Method, String> {
  const _CallMethodName();

  @override
  String visit(Method node) {
    if (node.isConstructor) {
      return 'globalEnv_NewObject';
    }
    final String type;
    if (node.returnType is PrimitiveType) {
      type = node.returnType.name.capitalize();
    } else {
      type = 'Object';
    }
    return 'globalEnv_Call${node.isStatic ? 'Static' : ''}${type}Method';
  }
}

class _OperatorGenerator extends Visitor<Method, void> {
  final Resolver resolver;
  final StringSink s;
  final Operator operator;

  _OperatorGenerator(this.resolver, this.s, {required this.operator});

  @override
  void visit(Method node) {
    final returnType = operator.returnsVoid
        ? 'void'
        : node.returnType.accept(_TypeGenerator(resolver));
    final paramsDef = node.params.accept(_ParamDef(resolver)).join(', ');
    final paramsCall = node.params.map((param) => param.finalName).join(', ');
    s.write('''
  $returnType operator ${operator.dartSymbol}($paramsDef) {
    ${operator.returnsVoid ? '' : 'return '}${node.finalName}($paramsCall);
  }
''');
  }
}

class _ComparatorGenerator extends Visitor<Method, void> {
  final Resolver resolver;
  final StringSink s;

  _ComparatorGenerator(this.resolver, this.s);

  @override
  void visit(Method node) {
    final paramsDef = node.params.accept(_ParamDef(resolver)).join(', ');
    final paramsCall = node.params.map((param) => param.finalName).join(', ');
    final name = node.finalName;
    s.write('''
  bool operator <($paramsDef) {
    return $name($paramsCall) < 0;
  }

  bool operator <=($paramsDef) {
    return $name($paramsCall) <= 0;
  }

  bool operator >($paramsDef) {
    return $name($paramsCall) > 0;
  }

  bool operator >=($paramsDef) {
    return $name($paramsCall) >= 0;
  }
''');
  }
}
